package jmine.tec.web.wicket.component.report;

import static jmine.tec.web.wicket.pages.form.WebFormPagesMessages.LABEL_SEARCH_RESULT;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import jmine.tec.report.api.RendererTarget;
import jmine.tec.report.api.Report;
import jmine.tec.report.api.ReportElement;
import jmine.tec.report.api.ReportTextBox;
import jmine.tec.report.api.table.ReportCell;
import jmine.tec.report.api.table.ReportColumn;
import jmine.tec.report.api.table.ReportRow;
import jmine.tec.report.api.table.ReportTable;
import jmine.tec.report.export.ReportFileExporter;
import jmine.tec.report.impl.table.CellValueResolver;
import jmine.tec.report.impl.table.RendererParameterInterface;
import jmine.tec.report.impl.table.ReportCellImpl;
import jmine.tec.report.impl.table.pdf.ReportCellPDFRenderer;
import jmine.tec.report.pdf.ReportPDFExporter;
import jmine.tec.report.xls.ReportXLSExporter;
import jmine.tec.web.wicket.component.InlinedLabel;
import jmine.tec.web.wicket.spring.SpringBeanReference;

import org.apache.wicket.AttributeModifier;
import org.apache.wicket.Component;
import org.apache.wicket.ResourceReference;
import org.apache.wicket.behavior.AttributeAppender;
import org.apache.wicket.behavior.SimpleAttributeModifier;
import org.apache.wicket.extensions.markup.html.repeater.data.grid.DataGridView;
import org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.image.Image;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.html.link.ResourceLink;
import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.markup.html.panel.EmptyPanel;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.IDataProvider;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;

/**
 * {@link ReportTableMarkupBuilder} para criar o markup de um {@link Report}
 * 
 * @author takeshi
 */
public class ReportTableMarkupBuilder implements Serializable {

    private static final int CEM = 100;

    private boolean includeButtons = true;

    private boolean includeHeaders = true;

    private boolean includeHeaderParameters = false;

    private final SpringBeanReference<ReportPDFExporter> pdfExporter = SpringBeanReference.forName("reportPDFExporter");

    private final SpringBeanReference<ReportXLSExporter> xlsExporter = SpringBeanReference.forName("reportXLSExporter");

    /**
     * Cria um componente com o markup inlined. Para se ter uma ideia de como deve ser feito o layout do painel, olhar ReportTablePanel.html
     * 
     * @param id {@link String}
     * @param report {@link Report}
     * @return {@link Component}
     */
    public WebMarkupContainer createInlineMarkup(String id, Report report) {
        WebMarkupContainer mainContainer = new WebMarkupContainer(id);
        mainContainer.add(new Label("title", LABEL_SEARCH_RESULT.create().getMessage()));

        Component pdfButton = this.createExportPDFLink(report);
        pdfButton.setVisible(this.isIncludeHeaders());
        mainContainer.add(pdfButton);
        Component xlsButton = this.createExportXLSLink(report);
        xlsButton.setVisible(this.isIncludeHeaders());
        mainContainer.add(xlsButton);

        if (this.isIncludeHeaderParameters()) {
            mainContainer.add(new HeaderContentPanel("header_content", report.getHeader()));
        } else {
            mainContainer.add(new EmptyPanel("header_content"));
        }

        List<ReportElement> elements = report.getElements();
        // TODO (renan) : criar um MarkupBuilder que não trate apenas ReportTable..
        List<ReportElement> tableElements = new ArrayList<ReportElement>();
        for (ReportElement element : elements) {
            if (!(element instanceof ReportTextBox)) {
                tableElements.add(element);
            }
        }

        ListView<ReportElement> lv = new ListView<ReportElement>("elements", tableElements) {

            @Override
            protected void populateItem(ListItem<ReportElement> item) {
                ReportTable<?> table = (ReportTable<?>) item.getModelObject();
                item.add(ReportTableMarkupBuilder.this.createTableHeader(table));
                item.add(ReportTableMarkupBuilder.this.createColumnHeader(table));
                ReportTableMarkupBuilder.this.createAndAddBody(item, table);
                item.add(ReportTableMarkupBuilder.this.createFooter(table));
            }
        };
        mainContainer.add(lv);
        return mainContainer;
    }

    /**
     * Cria o header de uma tabela
     * 
     * @param table {@link ReportTable}
     * @return {@link Component}
     */
    protected Component createTableHeader(ReportTable<?> table) {
        final String size = String.valueOf(table.getColumns().size());
        List<ReportCell> headers = table.getTableHeader();

        WebMarkupContainer header = new WebMarkupContainer("header");
        header.setVisible(!headers.isEmpty());
        header.add(new SimpleAttributeModifier("colspan", size));

        final double totalWidth = this.sumRelativeWidth(headers);

        ListView<ReportCell> cells = new ListView<ReportCell>("headers", headers) {

            @Override
            protected void populateItem(ListItem<ReportCell> item) {
                Label cell = new Label("cell", ReportTableMarkupBuilder.this.renderReportCell(item.getModelObject()));
                String widthCell = ReportTableMarkupBuilder.this.widthCalculate(item.getModelObject(), totalWidth);
                item.add(new SimpleAttributeModifier("width", widthCell));
                item.add(cell);
            }
        };

        header.add(cells);
        return header;
    }

    /**
     * Soma todas as larguras relativas
     * 
     * @param reportCells lista de celulas
     * @return total width
     */
    protected final double sumRelativeWidth(List<? extends RendererTarget> reportCells) {
        double sum = 0.0;
        for (RendererTarget cell : reportCells) {
            if (!this.createRPI(cell).isHidden()) {
                sum += this.createRPI(cell).getRelativeWidth();
            }
        }
        return sum;
    }

    /**
     * Cria um footer de uma tabela
     * 
     * @param <T> t
     * @param table {@link ReportTable}
     * @return {@link Component}
     */
    protected <T> Component createFooter(ReportTable<T> table) {
        List<ReportColumn<T>> columns = table.getColumns();
        List<ReportCell> header = new ArrayList<ReportCell>(columns.size());
        for (ReportColumn reportColumn : columns) {
            if (!this.hideColumn(reportColumn)) {
                ReportCell footer = reportColumn.getFooter();
                if (footer != null) {
                    header.add(footer);
                } else {
                    header.add(new ReportCellImpl(""));
                }
            }
        }
        ListView<ReportCell> lvCells = new ListView<ReportCell>("footer", header) {

            @Override
            protected void populateItem(ListItem<ReportCell> item) {
                item.add(new Label("cell", ReportTableMarkupBuilder.this.renderReportCell(item.getModelObject())));
            }
        };
        return lvCells;
    }

    /**
     * Cria um link para exportacao do relatorio em XLS
     * 
     * @param report {@link Report}
     * @return {@link Component}
     */
    private Component createExportXLSLink(final Report report) {
        Link<Void> link = this.createResourceLink("exportXLS", this.xlsExporter, report);
        link.add(new Image("icon", new ResourceReference(ReportTableMarkupBuilder.class, "spreadsheet.png")));
        return link;
    }

    /**
     * Cria um link para exportacao do relatorio em PDF
     * 
     * @param report {@link Report}
     * @return {@link Component}
     */
    private Component createExportPDFLink(Report report) {
        Link<Void> link = this.createResourceLink("exportPDF", this.pdfExporter, report);
        link.add(new Image("icon", new ResourceReference(ReportTableMarkupBuilder.class, "pdficon.gif")));
        return link;
    }

    /**
     * Cria uma nova instancia de um resourceLink de exportação.
     * 
     * @param id id
     * @param reportExporter spring reference para um {@link ReportFileExporter}.
     * @param report {@link Report} que vai ser exportado
     * @return {@link Link}
     */
    protected Link<Void> createResourceLink(String id, SpringBeanReference<? extends ReportFileExporter> reportExporter, Report report) {
        return new ResourceLink<Void>(id, new ExportReportWebResource(reportExporter, report));
    }

    /**
     * Cria o body de uma tabela.
     * 
     * @param item item aonde a tabela deve ser adicionada
     * @param table {@link ReportTable}
     * @return {@link Component}
     */
    protected DataGridView<ReportRow> createAndAddBody(ListItem<ReportElement> item, ReportTable<?> table) {
        DataGridView<ReportRow> grid = new DataGridView<ReportRow>("body", this.getCellPopulators(table), this.toDataProvider(table)) {

            @Override
            protected Item<ReportRow> newRowItem(String id, int index, IModel<ReportRow> model) {
                Item<ReportRow> rowItem = super.newRowItem(id, index, model);
                RendererParameterInterface rpi = new RendererParameterInterface(model.getObject());

                if (rpi.isGroup()) {
                    rowItem.add(new AttributeModifier("class", new Model<String>(String.format("reportTableGroupStyle%s ",
                            rpi.getGroupIndex()))) {

                        @Override
                        protected String newValue(String currentValue, String additionalValue) {
                            return currentValue + " " + additionalValue;
                        }

                    });
                }

                return rowItem;
            }

        };
        item.add(grid);
        return grid;
    }

    /**
     * cria uma lista com {@link ICellPopulator} para as linhas da tabela
     * 
     * @param <V> o tipo da tabela
     * @param table {@link ReportTable}
     * @return {@link List}
     */
    protected <V> List<ICellPopulator<ReportRow>> getCellPopulators(ReportTable<V> table) {
        List<ReportColumn<V>> columns = table.getColumns();
        List<ICellPopulator<ReportRow>> rows = new ArrayList<ICellPopulator<ReportRow>>(columns.size());
        for (final ReportColumn<V> reportColumn : columns) {
            if (!this.hideColumn(reportColumn)) {
                rows.add(new ICellPopulator<ReportRow>() {
                    public void detach() {
                    }

                    public void populateItem(Item<ICellPopulator<ReportRow>> cellItem, String componentId, IModel<ReportRow> rowModel) {
                        @SuppressWarnings("unchecked")
                        ReportRow<V> reportRow = rowModel.getObject();
                        CellValueResolver<V> cellValueResolver = reportColumn.getCellValueResolver();
                        if (cellValueResolver instanceof ComponentValueResolver) {
                            cellItem.add(((ComponentValueResolver<V>) cellValueResolver).createComponents(componentId,
                                    reportRow.getRecord()));
                        } else {
                            ReportCell reportCell = reportRow.getCellMap().get(reportColumn.getId());
                            cellItem.add(new InlinedLabel(componentId, ReportTableMarkupBuilder.this.renderReportCell(reportCell)));
                            String rendererType = reportColumn.getRendererType();
                            cellItem.add(new AttributeAppender("class", new Model<String>(rendererType + "Cell"), " "));
                        }
                    }
                });
            }
        }
        return rows;
    }

    /**
     * Verifica se uma coluna deve ou nao ser renderizada
     * 
     * @param reportColumn {@link ReportColumn}
     * @return boolean
     */
    private boolean hideColumn(ReportColumn<?> reportColumn) {
        return reportColumn.getParameters().getBoolean(RendererParameterInterface.PARAM_HIDDEN, false);
    }

    /**
     * Extrai o {@link IDataProvider} que deve ser utilizado para renderizar a {@link ReportTable} informada.
     * 
     * @param table {@link ReportTable}
     * @return {@link IDataProvider}
     */
    protected IDataProvider<ReportRow> toDataProvider(ReportTable<?> table) {
        return new ListDataProvider<ReportRow>(this.reportTableRows(table));
    }

    /**
     * Obtém a lista de todas as linhas do relatório
     * 
     * @param <V> record type
     * @param table table
     * @return lista de rows
     */
    protected <V> List<ReportRow> reportTableRows(ReportTable<V> table) {
        List<ReportRow> rows = new ArrayList<ReportRow>();
        for (Iterator<ReportRow<V>> it = table.createRowIterator(); it.hasNext();) {
            rows.add(it.next());
        }
        return rows;
    }

    /**
     * @param reportElement {@link ReportTable}
     * @return {@link Component}
     */
    @SuppressWarnings("unchecked")
    protected ListView<ReportCell> createColumnHeader(ReportTable reportElement) {
        List<ReportColumn> columns = reportElement.getColumns();
        List<ReportCell> header = new ArrayList<ReportCell>(columns.size());

        for (ReportColumn reportColumn : columns) {
            if (!this.hideColumn(reportColumn)) {
                header.add(reportColumn.getHeader());
            }
        }

        return this.doCreateHeaderListView(header);
    }

    /**
     * Cria o {@link ListView} ta tabela de header.
     * 
     * @param header colunas que devem ser adicionadas no header
     * @return a {@link ListView}
     */
    protected ListView<ReportCell> doCreateHeaderListView(List<ReportCell> header) {
        final double totalWidth = this.sumRelativeWidth(header);

        return new ListView<ReportCell>("columns", header) {

            @Override
            protected void populateItem(ListItem<ReportCell> item) {
                ReportCell reportCell = item.getModelObject();
                Label cell = new Label("cell", ReportTableMarkupBuilder.this.renderReportCell(reportCell));
                String widthCell = ReportTableMarkupBuilder.this.widthCalculate(reportCell, totalWidth);
                cell.add(new SimpleAttributeModifier("width", widthCell));
                item.add(cell);
            }

        };
    }

    /**
     * Calcula a largura
     * 
     * @param reportCell reportCell
     * @param totalWidth totalWidth
     * @return a largura
     */
    protected final String widthCalculate(ReportCell reportCell, double totalWidth) {
        return String.format("%s%s", Math.round(CEM * this.createRPI(reportCell).getRelativeWidth() / totalWidth), "%");
    }

    /**
     * Renderiza uma cell como uma string usando um renderer de pdf
     * 
     * @param modelObject {@link ReportCell}
     * @return String
     */
    protected String renderReportCell(ReportCell modelObject) {
        ReportCellPDFRenderer renderer = this.pdfExporter.get().getCellRendererRegistry().getRenderer(modelObject.getRendererType());
        return renderer == null ? String.valueOf(modelObject.getValue()) : renderer.getFormattedString(modelObject);
    }

    /**
     * @return the includeButtons
     */
    public boolean isIncludeButtons() {
        return this.includeButtons;
    }

    /**
     * @param includeButtons the includeButtons to set
     */
    public void setIncludeButtons(boolean includeButtons) {
        this.includeButtons = includeButtons;
    }

    /**
     * @return the includeHeaders
     */
    public boolean isIncludeHeaders() {
        return this.includeHeaders;
    }

    /**
     * @param includeHeaders the includeHeaders to set
     */
    public void setIncludeHeaders(boolean includeHeaders) {
        this.includeHeaders = includeHeaders;
    }

    /**
     * @return the includeHeaderParameters
     */
    public boolean isIncludeHeaderParameters() {
        return this.includeHeaderParameters;
    }

    /**
     * @param includeHeaderParameters the includeHeaderParameters to set
     */
    public void setIncludeHeaderParameters(boolean includeHeaderParameters) {
        this.includeHeaderParameters = includeHeaderParameters;
    }

    /**
     * Cria um RendererParameterInterface para o target
     * 
     * @param target target
     * @return RendererParameterInterface
     */
    protected final RendererParameterInterface createRPI(RendererTarget target) {
        return new RendererParameterInterface(target);
    }
}
